package com.xiaoxin.experience.service.hls.common;

import com.xiaoxin.experience.util.DownloadUtil;
import com.xiaoxin.experience.util.JSONUtil;
import com.xiaoxin.experience.util.StringUtil;
import com.xiaoxin.experience.util.UrlUtil;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * @author xiaoxin
 */
@Slf4j
public class HlsParser
{
    static
    {
        Security.addProvider(new BouncyCastleProvider());
    }

    private String hlsDownloadUrl;

    private String realHlsDownloadUrl;

    private String hlsIndex;

    private HlsEncryptInfo hlsEncryptInfo = new HlsEncryptInfo();

    private final List<String> tsDownloadPaths = new ArrayList<>();

    private int retryTimes = 3;

    public static HlsParser newInstance(String hlsDownloadUrl)
    {
        return new HlsParser(hlsDownloadUrl);
    }

    public HlsParser(String hlsDownloadUrl)
    {
        this.hlsDownloadUrl = hlsDownloadUrl;
    }

    private HlsParser()
    {

    }

    public HlsParser retryTimes(int retryTimes)
    {
        if (retryTimes > 0)
        {
            this.retryTimes = retryTimes;
        }
        return this;
    }

    private String getHlsIndex(String hlsDownloadUrl)
    {
        byte[] bytes = DownloadUtil.downloadLimitRetry(hlsDownloadUrl,retryTimes);
        if (Objects.isNull(bytes))
        {
            throw new RuntimeException("download hls index fail!");
        }
        String hlsIndex = new String(bytes, StandardCharsets.UTF_8);
        if (hlsIndex.contains("EXTINF"))
        {
            realHlsDownloadUrl = hlsDownloadUrl;
            log.info("provide hlsDownloadUrl is {}, real hlsDownloadUrl is {}", this.hlsDownloadUrl, realHlsDownloadUrl);
            return hlsIndex;
        }
        else
        {
            String[] lines = hlsIndex.split("[\r\n]");
            for (String line : lines)
            {
                if (line.contains(".m3u8"))
                {
                    hlsDownloadUrl = hlsDownloadUrlParse(line,hlsDownloadUrl);
                    break;
                }
            }
        }
        return getHlsIndex(hlsDownloadUrl);
    }


    private void hlsIndexParse()
    {
        String[] lines = hlsIndex.split("[\r\n]");
        for (int i = 0; i < lines.length; i++)
        {
            String line = lines[i];
            // 如果含有此字段,则添加下一行数据,下一行数据为Ts文件下载路径
            if (line.contains("EXTINF"))
            {
                i++;
                line = lines[i];
                while (StringUtil.isBlank(line))
                {
                    i++;
                    line = lines[i];
                }
                line = line.trim();
                tsDownloadPaths.add(hlsDownloadUrlParse(line,realHlsDownloadUrl));
                continue;
            }
            //如果含有此字段，则获取加密算法以及获取密钥的链接
            if (line.contains("EXT-X-KEY"))
            {
                fullEncryptInfo(line);
            }
        }
    }

    private void fullEncryptInfo(String encryptInfo)
    {
        String[] encryptArray = encryptInfo.split(",");
        for (String s1 : encryptArray)
        {
            if (s1.contains("METHOD"))
            {
                String method = s1.split("=", 2)[1];
                hlsEncryptInfo.setMethod(method);
            }
            else if (s1.contains("URI"))
            {
                String keyDownloadUrl = s1.split("=", 2)[1];
                keyDownloadUrl = keyDownloadUrl.replace("\"", "");
                byte[] bytes = DownloadUtil.downloadLimitRetry(hlsDownloadUrlParse(keyDownloadUrl,realHlsDownloadUrl),retryTimes);
                if (Objects.isNull(bytes))
                {
                    throw new RuntimeException("download decrypt key fail!");
                }
                if (bytes.length == 16)
                {
                    hlsEncryptInfo.setKeyBytes(Arrays.copyOf(bytes, 16));
                }
                else
                {
                    hlsEncryptInfo.setKey(new String(Arrays.copyOf(bytes, bytes.length)));
                }
            }
            else if (s1.contains("IV"))
            {
                String iv = s1.split("=", 2)[1];
                hlsEncryptInfo.setIv(iv);
            }
        }
    }

    private String hlsDownloadUrlParse(String indexUrl,String hlsUrl)
    {
        if (indexUrl.startsWith("http"))
        {
            return indexUrl;
        }
        if (indexUrl.startsWith("/"))
        {
            return UrlUtil.website(hlsUrl) + indexUrl;
        }
        return UrlUtil.relativeURL(hlsUrl) + indexUrl;
    }

    public HlsEncryptInfo getHlsEncryptInfo()
    {
        log.info("hls encrypt info : {}", hlsEncryptInfo);
        return hlsEncryptInfo;
    }

    public List<String> getTsDownloadPaths()
    {
        log.info("ts total count: {}", tsDownloadPaths.size());
        return tsDownloadPaths;
    }

    public byte[] decrypt(byte[] encrypt)
    {
        if (hlsEncryptInfo.isEmpty())
        {
            return encrypt;
        }
        try
        {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
            String iv = hlsEncryptInfo.getIv();
            byte[] key = hlsEncryptInfo.getKey();
            SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
            byte[] ivByte;
            if (iv.startsWith("0x"))
            {
                ivByte = StringUtil.hexStringToByteArray(iv.substring(2));
            }
            else
            {
                ivByte = iv.getBytes();
            }
            if (ivByte.length != 16)
            {
                ivByte = new byte[16];
            }
            //如果m3u8有IV标签，那么IvParameterSpec构造函数就把IV标签后的内容转成字节数组传进去
            AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
            return cipher.doFinal(encrypt, 0, encrypt.length);
        }
        catch (Exception e)
        {
            return encrypt;
        }
    }

    public String getHlsIndex()
    {
        return hlsIndex;
    }

    public HlsParser parse()
    {
        this.hlsIndex = getHlsIndex(hlsDownloadUrl);
        hlsIndexParse();
        return this;
    }

    public static HlsParser load(String path)
    {
        try(BufferedReader br = new BufferedReader(new FileReader(path)))
        {
            HlsParser hlsParser = new HlsParser();
            String s = br.readLine();
            hlsParser.hlsEncryptInfo = JSONUtil.fromJson(s, HlsEncryptInfo.class);
            br.lines().forEach(line->{
                if (line.startsWith("http"))
                {
                    hlsParser.tsDownloadPaths.add(line);
                }
            });
            return hlsParser;
        }
        catch (Exception e)
        {
            throw new RuntimeException("load hls message fail");
        }
    }

}
